package com.chushouya.common.applet;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.chushouya.common.enums.RedisKeys;
import com.chushouya.common.enums.RedisLocks;
import com.general.framework.core.exception.Ex;
import com.general.framework.core.lang.Jsons;
import com.general.framework.core.lang.Strings;
import com.general.framework.core.lang.Validate;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 小红书小程序客户端
 */
@Slf4j
public class XiaoHongShuClient {
    
    private static final int SUCCESS_CODE = 0;
    
    /** 获取openId */
    private static final String SESSION_URL = "https://miniapp.xiaohongshu.com/api/rmp/session";
    
    /** 获取小程序accessToken */
    private static final String ACCESS_TOKEN_URL = "https://miniapp.xiaohongshu.com/api/rmp/token";
    
    private final String appId;
    private final String secret;

    public XiaoHongShuClient(String appId, String secret) {
        this.appId = appId;
        this.secret = secret;
    }

    /**
     * 小红书授权登录 - code换取openId
     * 
     * @param code 小程序登录凭证
     * @return 用户openId和sessionKey的Map
     */
    public String login(String code) {
        Validate.isBlank(code, "code不能为空");

        // 获取accessToken
        String accessToken = getAccessToken();
        
        Map<String, Object> params = new HashMap<>();
        params.put("appid", appId);
        params.put("access_token", accessToken);
        params.put("code", code);

        String url = SESSION_URL + "?" + URLUtil.buildQuery(params, StandardCharsets.UTF_8);
        String resp = HttpUtil.get(url);
        log.debug("小红书授权登录响应:{}", resp);
        
        JSONObject json = Jsons.parseObject(resp);
        if (json.getInteger("code") != null && json.getInteger("code") != SUCCESS_CODE) {
            log.error("小红书授权登录异常: {}", resp);
            throw Ex.business("小红书授权登录异常:{}", json.getString("msg"));
        }
        
        JSONObject data = json.getJSONObject("data");
        String openId = data.getString("openid");
        String sessionKey = data.getString("session_key");
        
        if (Strings.isBlank(openId)) {
            throw Ex.business("获取openId失败");
        }

        // 保存sessionKey到Redis
        saveSessionKey(openId, sessionKey);
        return openId;
    }

    /**
     * 获取手机号
     * 
     * @param encryptedData 加密数据
     * @param openId 用户openId
     * @param iv 初始向量
     * @return 手机号码
     */
    public String getPhoneNumber(String encryptedData, String openId, String iv) {
        try {
            // 获取保存的sessionKey
            String sessionKey = getSessionKey(openId);
            if (Strings.isBlank(sessionKey)) {
                throw new Exception("未找到会话密钥，请重新登录");
            }
            
            // 解密数据
            String decryptedData = decrypt(encryptedData, sessionKey, iv);
            JSONObject data = Jsons.parseObject(decryptedData);
            String phone = data.getString("phoneNumber");
            
            if (Strings.isBlank(phone)) {
                throw new Exception("解密后未找到手机号");
            }
            
            return phone;
        } catch (Exception e) {
            log.error("获取手机号失败: {}", e.getMessage(), e);
            throw Ex.business("获取手机号失败: {}", e.getMessage());
        }
    }

    /**
     * AES-128-CBC对称解密
     * 算法：AES-128-CBC，数据采用PKCS#7填充
     * 
     * @param encryptedData 加密的数据(Base64编码)
     * @param sessionKey 会话密钥(Base64编码)
     * @param iv 初始向量(Base64编码)
     * @return 解密后的数据
     * @throws Exception 解密异常
     */
    private String decrypt(String encryptedData, String sessionKey, String iv) throws Exception {
        log.info("开始解密数据: encryptedData={}\nsessionKey={}\niv={}", encryptedData, sessionKey, iv);
        try {
            // Base64解码
            byte[] sessionKeyBytes = Base64.decode(sessionKey);
            byte[] ivBytes = Base64.decode(iv);
            byte[] encryptedBytes = Base64.decode(encryptedData);
            
            // 创建AES密钥
            SecretKeySpec keySpec = new SecretKeySpec(sessionKeyBytes, "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
            
            // 创建解密器
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            
            // 解密
            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
            
            return new String(decryptedBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("解密失败: {}", e.getMessage(), e);
            throw new Exception("解密失败: " + e.getMessage());
        }
    }

    /**
     * 保存sessionKey到Redis
     * 
     * @param openId 用户openId
     * @param sessionKey 会话密钥
     */
    private void saveSessionKey(String openId, String sessionKey) {
        String redisKey = Strings.format(RedisKeys.SESSION_KEY, appId, openId);
        // 保存30天
        RedisHolder.REDIS_CACHE.setCacheObject(redisKey, sessionKey, 30L, TimeUnit.DAYS);
    }

    /**
     * 从Redis获取sessionKey
     * 
     * @param openId 用户openId
     * @return 会话密钥
     */
    private String getSessionKey(String openId) {
        String redisKey = Strings.format(RedisKeys.SESSION_KEY, appId, openId);
        return RedisHolder.REDIS_CACHE.getCacheObject(redisKey);
    }

    /**
     * 获取小程序accessToken
     * 先从缓存读取，不存在则请求小红书获取最新的token，获取成功后缓存起来
     * 
     * @return accessToken
     */
    private String getAccessToken() {
        final String accessTokenRedisKey = Strings.format(RedisKeys.ACCESS_TOKEN, appId);
        if (RedisHolder.REDIS_CACHE.exist(accessTokenRedisKey)) {
            return RedisHolder.REDIS_CACHE.getCacheObject(accessTokenRedisKey);
        }

        // 不存在获取最新
        RLock lock = RedisHolder.REDISSON_CLIENT.getLock(Strings.format(RedisLocks.ACCESS_TOKEN_LOCK, appId));
        try {
            // 获取锁超时
            if (!lock.tryLock(5, TimeUnit.SECONDS)) {
                throw Ex.business("服务繁忙, 请稍后重试");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        try {
            // 双重检查
            if (RedisHolder.REDIS_CACHE.exist(accessTokenRedisKey)) {
                return RedisHolder.REDIS_CACHE.getCacheObject(accessTokenRedisKey);
            }

            JSONObject json = requestAccessToken();
            if (json == null) {
                throw Ex.business("获取accessToken异常");
            }
            
            JSONObject data = json.getJSONObject("data");
            String accessToken = data.getString("access_token");
            if (Strings.isBlank(accessToken)) {
                throw Ex.business(json.containsKey("msg") ? json.getString("msg") : "获取accessToken异常");
            }
            
            // 缓存accessToken
            Long expiresIn = data.getLong("expire_in");
            if (expiresIn == null) {
                expiresIn = 7200L; // 默认2小时
            }
            RedisHolder.REDIS_CACHE.setCacheObject(accessTokenRedisKey, accessToken, expiresIn, TimeUnit.SECONDS);
            return accessToken;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 从小红书获取最新的accessToken
     * 
     * @return accessToken响应数据
     */
    public JSONObject requestAccessToken() {
        Map<String, Object> params = new HashMap<>();
        params.put("appid", appId);
        params.put("secret", secret);

        String resp = HttpUtil.post(ACCESS_TOKEN_URL, Jsons.toJsonString(params));
        log.debug("从小红书获取最新的accessToken响应:{}", resp);
        
        JSONObject json = Jsons.parseObject(resp);
        if (json.getInteger("code") != null && json.getInteger("code") != SUCCESS_CODE) {
            log.error("小红书获取accessToken异常: {}", resp);
            throw Ex.business(json.getString("msg"));
        }
        return json;
    }

    /**
     * token过期删除缓存
     */
    public void deleteAccessToken() {
        RedisHolder.REDIS_CACHE.deleteObject(Strings.format(RedisKeys.ACCESS_TOKEN, appId));
    }
}
